iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
Mobile Development

結合AI Agent技術打造自己的行程管家系列 第 29

Day 29行程管家整合篇:前端溝通橋梁誕生,小助手開始對話!(前端2)

  • 分享至 

  • xImage
  •  

昨天,我們完成了行程管家小助手的前端架構基礎,定義了 Message 與 ChatRequest 等資料模型,為互動邏輯鋪好了第一塊地基。
而今天,我們要邁出關鍵一步 —— 讓行程管家小助手真正與使用者互動!
也就是說,當使用者輸入訊息後,系統不僅要能回覆,還要能根據回應內容觸發對應的地圖動作,形成「前端 ↔ 後端 ↔ 使用者」的完整溝通橋梁。

今日目標

  1. 建立聊天訊息顯示的 RecyclerView Adapter
  2. 串接 API 並動態生成 AI 回覆
  3. 根據不同回覆內容,觸發地圖顯示或導航功能

MessageAdapter.java的視窗畫面程式碼

package com.example.ittext.ui.chat;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.example.ittext.R;


import java.util.List;

public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MessageViewHolder> {

        private List<Message> messageList;
        private OnActionClickListener actionListener;

        public interface OnActionClickListener {
            void onMapActionClick(MessageAction action, Object data);
        }

        public MessageAdapter(List<Message> messageList, OnActionClickListener listener) {
            this.messageList = messageList;
            this.actionListener = listener;
        }

        @NonNull
        @Override
        public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.chat_message_item, parent, false);
            return new MessageViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) {
            Message message = messageList.get(position);

            holder.messageText.setText(message.getContent());

            if (message.isUser()){
                holder.messageText.setBackgroundResource(R.drawable.bg_message_sent);
            }else {
                holder.messageText.setBackgroundResource(R.drawable.bg_message_received);
            }



            // 如果訊息包含地圖動作,顯示按鈕
            if (message.getAction() != MessageAction.NONE) {
                holder.actionButton.setVisibility(View.VISIBLE);
                holder.actionButton.setText(getActionButton(message.getAction()));
                holder.actionButton.setVisibility(View.VISIBLE);
                holder.itemView.setOnClickListener(v -> {
                    if (actionListener != null) {
                        actionListener.onMapActionClick(message.getAction(), message.getActionData());
                    }
                });
            } else {
                holder.itemView.setVisibility(View.GONE);
            }
        }

        private String getActionButton(MessageAction action) {
            switch (action) {
                case OPEN_MAP: return "查看地圖";
                case SHOW_ROUTE: return "查看路線";
                case SHOW_LOCATION: return "查看景點";
                default: return "";
            }
        }
        public static class MessageViewHolder extends RecyclerView.ViewHolder {
        TextView messageText;
        Button actionButton;

        public MessageViewHolder(@NonNull View itemView) {
            super(itemView);
            messageText = itemView.findViewById(R.id.message_text);
            actionButton = itemView.findViewById(R.id.action_button);
        }
    }

    @Override
    public int getItemCount() {
        return messageList != null ? messageList.size() : 0;
    }


}

chatAPI的程式碼

private void getChat(String message) {
        ChatRequest chatRequest = new ChatRequest(message);

        Log.d("=== 發送請求 ===" , "訊息: " + message);
        getApi.getChat(chatRequest)
                .subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
                .observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread())
                .subscribe(new io.reactivex.rxjava3.core.Observer<ChatResponse>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {
                        compositeDisposable.add(d);
                    }

                    @Override
                    public void onNext(@NonNull ChatResponse chatResponse) {

                        Log.d("=== API 回應成功 ===", "完整回應: " + (chatResponse != null ? chatResponse.toString() : "null"));

                        if (chatResponse == null || chatResponse.getReply() == null || chatResponse.getReply().isEmpty()) {
                            Log.e("ChatActivity", "Error: Reply is null or empty, Response: " + (chatResponse != null ? chatResponse.toString() : "null"));
                            messageList.add(new Message("抱歉,我沒有收到有效的回應。", false));
                            messageAdapter.notifyItemInserted(messageList.size() - 1);
                            ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);
                            return;
                        }

                            Message aiMessage = generateAIResponse(chatResponse);
                            messageList.add(aiMessage);
                            messageAdapter.notifyItemInserted(messageList.size() - 1);
                            ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);

                        // 如果 AI 回覆包含地圖動作,則觸發地圖功能
                        if (aiMessage.getAction() != null) {
                            handleMapAction(aiMessage.getAction(), aiMessage.getActionData());
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                       // 顯示錯誤訊息

                        Log.d("=== API 錯誤 ===", "Error type: " + e.getClass().getSimpleName());
                        Log.d("=== API 錯誤 ===", "Error message: " + e.getMessage());

                        if (e instanceof HttpException) {
                            HttpException httpException = (HttpException) e;
                            int code = httpException.code();
                            Log.d("TAG", "HTTP Status Code: " + code);

                            try {
                                String errorBody = httpException.response().errorBody().string();
                                Log.d("TAG", "Error Body: " + errorBody);
                            } catch (IOException ioException) {
                                Log.d( "Cannot read error body","ioException", ioException);
                            }
                        } else if (e instanceof IOException) {
                            Log.d("TAG", "Network Error: " + e.getMessage());
                        } else {
                            Log.d("TAG", "Unexpected Error: " + e.getMessage());
                        }


                        Toast.makeText(ChatActivity.this, "訊息發送失敗,請稍後再試", Toast.LENGTH_SHORT).show();
                        messageList.add(new Message("抱歉,系統發生錯誤,請稍後再試。", false));
                        messageAdapter.notifyItemInserted(messageList.size() - 1);
                        ChatRecyclerView.smoothScrollToPosition(messageList.size() - 1);
                    }

                    @Override
                    public void onComplete() {
                        Log.d("=== API 請求完成 ===", "onComplete called");
                    }
                });
                }


    private Message generateAIResponse(ChatResponse chatResponse ) {
        String replyText = chatResponse.getReply();

        // 景點推薦
        if (chatResponse.getAction() != null && !chatResponse.getAction().isEmpty()) {
            Log.d("TAG", "Response has action: " + chatResponse.getAction());

            switch (chatResponse.getAction()) {
                case "SHOW_LOCATION":
                    return new Message(
                            replyText,
                            false,
                            MessageAction.SHOW_LOCATION,
                            chatResponse.getPlaces()
                    );

                case "SHOW_ROUTE":
                    RouteData routeData = new RouteData(
                            chatResponse.getStartPoint(),
                            chatResponse.getEndPoint(),
                            chatResponse.getWaypoints()
                    );
                    return new Message(
                            replyText,
                            false,
                            MessageAction.SHOW_ROUTE,
                            routeData
                    );

                case "OPEN_MAP":
                    return new Message(
                            replyText,
                            false,
                            MessageAction.OPEN_MAP,
                            null
                    );
            }
        }

        // 一般文字回應
        return new Message(replyText, false);
    }

    // 處理地圖動作
    private void handleMapAction(MessageAction action, Object data) {
        Intent intent = new Intent(this, MapsActivity.class);

        switch (action) {
            case SHOW_LOCATION:
                intent.putExtra("action", "show_locations");
                if (data instanceof List) {
                    intent.putStringArrayListExtra("places", new ArrayList<>((List<String>) data));
                }
                break;

            case SHOW_ROUTE:
                intent.putExtra("action", "show_route");
                if (data instanceof RouteData) {
                    RouteData route = (RouteData) data;
                    intent.putExtra("start", route.start);
                    intent.putExtra("end", route.end);
                    intent.putStringArrayListExtra("waypoints", new ArrayList<>(route.waypoints));
                }
                break;

            case OPEN_MAP:
                intent.putExtra("action", "open_map");
                break;
        }

        startActivity(intent);
    }

    // 路線數據類別
    static class RouteData {
        String start;
        String end;
        List<String> waypoints;

        RouteData(String start, String end, List<String> waypoints) {
            this.start = start;
            this.end = end;
            this.waypoints = waypoints;
        }
    }

下一篇,行程管家最終篇:三十天的實作旅程總結!


上一篇
Day 28 行程管家整合篇:前端溝通橋梁誕生,小助手開始對話!(前端1)
下一篇
Day 30|行程管家最終篇:三十天的實作旅程總結!
系列文
結合AI Agent技術打造自己的行程管家30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言